﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Mvc;
using System.Web.Routing;
using Mindfor.Web.Data;
using Mindfor.Web.Routing;
using NHibernate;
using NHibernate.Linq;
using Mindfor.Web.Models;

namespace Mindfor.Web
{
	/// <summary>
	/// Provides properties and functions to register module.
	/// </summary>
	public abstract class ModuleBase
	{
		string m_name;
		bool m_isEnabled;
		ControllerDescriptor[] m_controllers;
		Dictionary<string, ActionRouteInfo> m_actionRouteInfos;

		/// <summary>
		/// Gets or sets (protected) module name.
		/// If not changed then returns class name -"Module".
		/// </summary>
		public virtual string Name
		{
			get
			{
				if (m_name == null)
					m_name = GetModuleName();
				return m_name;
			}
			protected set { m_name = value; }
		}

		/// <summary>
		/// Gets friendly name. Default equals Name.
		/// </summary>
		public virtual string FriendlyName
		{
			get { return Name; }
		}

		/// <summary>
		/// Gets or sets if module is enabled.
		/// </summary>
		public bool IsEnabled
		{
			get { return m_isEnabled; }
			set
			{
				if (IsAlwaysEnabled)
					m_isEnabled = true;
				else
					m_isEnabled = value;
			}
		}

		/// <summary>
		/// Gets or sets if module can be disabled.
		/// </summary>
		public bool IsAlwaysEnabled { get; protected set; }

		/// <summary>
		/// Gets or sets state.
		/// </summary>
		public ModuleState State { get; internal set; }

		/// <summary>
		/// Gets or sets module registration error.
		/// </summary>
		public Exception RegistrationError { get; internal set; }

		/// <summary>
		/// Gets module options. Initialized only in Register.
		/// </summary>
		public ModuleOptionCollection Options { get; private set; }

		/// <summary>
		/// Initializes new instance.
		/// </summary>
		public ModuleBase()
		{
			IsEnabled = true;
			IsAlwaysEnabled = false;
			State = ModuleState.NotRegistered;
		}

		/// <summary>
		/// Registers module. For example, custom routes.
		/// </summary>
		/// <param name="info">Registration data.</param>
		public virtual void Register(ModuleRegistrationData info)
		{
			Options = new ModuleOptionCollection(Name, info.Data.Query<ModuleOption>().Where(r => r.ModuleName == Name).ToList());
		}

		/// <summary>
		/// Called when all modules was registered.
		/// </summary>
		/// <param name="info">Registration data.</param>
		/// <param name="registeredModules">Registered modules.</param>
		public virtual void AfterRegister(ModuleRegistrationData info, ModuleBase[] registeredModules)
		{
		}

		/// <summary>
		/// Called when application is ending.
		/// </summary>
		public virtual void Unload()
		{
		}

		/// <summary>
		/// Returns module name from class name.
		/// </summary>
		private string GetModuleName()
		{
			string className = this.GetType().Name;
			if (className.EndsWith("Module"))
				return className.Remove(className.LastIndexOf("Module"));
			return className;
		}

		/// <summary>
		/// Returns modules names current one dependent on.
		/// </summary>
		public virtual string[] GetDependencyModules()
		{
			return new string[0];
		}

		/// <summary>
		/// Performs search in module.
		/// </summary>
		/// <param name="data">Current data session.</param>
		/// <param name="url">Url helper.</param>
		/// <param name="text">Text to search.</param>
		/// <returns>Search results.</returns>
		public virtual IEnumerable<SearchResult> Search(ISession data, UrlHelper url, string text)
		{
			return new SearchResult[0];
		}

		/// <summary>
		/// Returns controllers from this module.
		/// By default, searches for controllers in "{module namespace}.Controllers" namespace.
		/// </summary>
		public virtual ControllerDescriptor[] GetControllers()
		{
			// search for controllers
			if (m_controllers == null)
			{
				List<ControllerDescriptor> controllers = new List<ControllerDescriptor>();
				Assembly asm = Assembly.GetExecutingAssembly();
				Type thisType = this.GetType();
				string ns = thisType.Namespace;
				if (!String.IsNullOrEmpty(ns))
					ns += ".";
				ns += "Controllers";

				foreach (Type type in TypeFinder.GetTypesFromCurrentDomain(ns))
				{
					if (type.GetInterfaces().Contains(typeof(IController)))
						controllers.Add(new ReflectedControllerDescriptor(type));
				}

				m_controllers = controllers.ToArray();
			}

			return m_controllers;
		}

		/// <summary>
		/// Returns controller from this module by name.
		/// </summary>
		/// <param name="controllerName">Controller name to search for.</param>
		public ControllerDescriptor GetController(string controllerName)
		{
			foreach (ControllerDescriptor c in GetControllers())
			{
				if (c.ControllerName == controllerName)
					return c;
			}
			return null;
		}

		/// <summary>
		/// Returns descriptors for controller actions of this module.
		/// </summary>
		/// <param name="includeMainActions">True to include main actions.</param>
		/// <param name="includeSubActions">True to include sub actions.</param>
		/// <param name="includeWidgetActions">True to include widget actions.</param>
		public ActionDescriptor[] GetActionDescriptors(bool includeMainActions, bool includeSubActions, bool includeWidgetActions)
		{
			// search for actions
			List<ActionDescriptor> res = new List<ActionDescriptor>();
			foreach (ControllerDescriptor controller in GetControllers())
			{
				foreach (ActionDescriptor action in controller.GetCanonicalActions())
				{
					// determine if action matches
					UrlSubActionAttribute urlSubActionAttribute = action.GetCustomAttribute<UrlSubActionAttribute>(true);
					WidgetAttribute widgetAttribute = action.GetCustomAttribute<WidgetAttribute>(true);
					bool isWidget = widgetAttribute != null;
					bool isMain = urlSubActionAttribute == null || String.IsNullOrEmpty(urlSubActionAttribute.MainAction);

					if (isWidget && includeWidgetActions ||
						!isWidget && isMain && includeMainActions ||
						!isWidget && !isMain && includeSubActions)
					{
						res.Add(action);
					}
				}
			}

			return res.ToArray();
		}

		/// <summary>
		/// Returns paths for controller actions of this module.
		/// </summary>
		/// <param name="includeMainActions">True to include main actions.</param>
		/// <param name="includeSubActions">True to include sub actions.</param>
		/// <param name="includeWidgetActions">True to include widget actions.</param>
		public ActionPath[] GetActionPaths(bool includeMainActions, bool includeSubActions, bool includeWidgetActions)
		{
			// search for actions
			List<ActionPath> res = new List<ActionPath>();
			foreach (ControllerDescriptor controller in GetControllers())
			{
				var actionsGroupped = controller.GetCanonicalActions().GroupBy(r => r.ActionName);
				foreach (var group in actionsGroupped)
				{
					// determine if action matches
					bool isMain = !group.Any(r => r.GetCustomAttribute<UrlSubActionAttribute>(true) != null);
					bool isWidget = group.Any(r => r.GetCustomAttribute<WidgetAttribute>(true) != null);
					
					if (isWidget && includeWidgetActions ||
						!isWidget && isMain && includeMainActions ||
						!isWidget && !isMain && includeSubActions)
					{
						res.Add(new ActionPath(Name, controller.ControllerName, group.Key));
					}
				}
			}

			return res.ToArray();
		}

		/// <summary>
		/// Returns ActionRouteInfo for each action from this module.
		/// </summary>
		public ActionRouteInfo[] GetActionRoutes()
		{
			if (m_actionRouteInfos == null)
			{
				m_actionRouteInfos = new Dictionary<string, ActionRouteInfo>();

				// find each action
				foreach (ControllerDescriptor controller in GetControllers())
				{
					foreach (ActionDescriptor action in controller.GetCanonicalActions())
					{
						// skip if needed
						if (action.GetCustomAttribute<UrlRouteSkipAttribute>(true) != null ||
							action.GetCustomAttribute<NonActionAttribute>(true) != null)
							continue;

						// get or create ActionRouteInfo
						string key = controller.ControllerName + "=>" + action.ActionName;
						ActionRouteInfo actionRoute;
						if (m_actionRouteInfos.ContainsKey(key))
							actionRoute = m_actionRouteInfos[key];
						else
						{
							actionRoute = new ActionRouteInfo(Name, controller.ControllerName, action.ActionName);
							m_actionRouteInfos.Add(key, actionRoute);
						}

						// get UrlSubActionAttribute
						UrlSubActionAttribute urlSubAction = action.GetCustomAttribute<UrlSubActionAttribute>(true);
						if (urlSubAction != null)
							actionRoute.MainAction = urlSubAction.MainAction;

						// add each UrlRouteAttribute
						UrlRouteAttribute[] urlRoutes = action.GetCustomAttributes<UrlRouteAttribute>(true);
						if (urlRoutes.Length > 0)
						{
							List<int> urlRoutesRegistered = new List<int>();
							foreach (UrlRouteAttribute urlRoute in urlRoutes)
							{
								if (urlRoutesRegistered.Contains(urlRoute.RouteIndex))
									throw new InvalidOperationException(String.Format("MVC Action can not contains 2 routes with index \"{0}\".", urlRoute.RouteIndex));
								urlRoutesRegistered.Add(urlRoute.RouteIndex);

								UrlRouteInfo route = new UrlRouteInfo(urlRoute.Name, urlRoute.Path, urlRoute.SubPath);
								actionRoute.Routes.Add(route);

								UrlRouteDefaultAttribute[] defaultAttrs = action.GetCustomAttributes<UrlRouteDefaultAttribute>(true);
								foreach (UrlRouteDefaultAttribute urlRouteDefault in defaultAttrs)
								{
									if (urlRouteDefault.RouteIndex == urlRoute.RouteIndex)
										route.Defaults[urlRouteDefault.Name] = urlRouteDefault.Value;
								}

								UrlRouteConstraintAttribute[] constraintsAttrs = action.GetCustomAttributes<UrlRouteConstraintAttribute>(true);
								foreach (UrlRouteConstraintAttribute urlRouteConstraint in constraintsAttrs)
								{
									if (urlRouteConstraint.RouteIndex == urlRoute.RouteIndex)
									{
										IRouteConstraint constraint = urlRouteConstraint.CreateConstraint();
										if (constraint != null)
											route.Constraints[urlRouteConstraint.Name] = constraint;
										else if (urlRouteConstraint.Regex != null)
											route.Constraints[urlRouteConstraint.Name] = urlRouteConstraint.Regex;
									}
								}
							}
						}
						else
						{
							// if no UrlRouteAttribute then register default route info
							UrlRouteInfo route = new UrlRouteInfo();
							actionRoute.Routes.Add(route);
						}
					}
				}
			}

			return m_actionRouteInfos.Values.ToArray();
		}
	}
}
